首页 / 技术类 / COM / 让 COM 脱离注册表

让 COM 脱离注册表

2012-09-21 00:34:00

引言

在上一篇《在 DLL 中加入第二个 COM 类》的“单用户注册”一节中,我们曾提到脱离注册表依赖一事,现在我们来把这事儿给办了。

注册

我们在之前支持了“regsvr32 /n /i:user COMProvider.dll”这一注册命令。这一注册命令给了我们一定的扩展余地。从ATL默认的代码来看,对于DllInstall,目前已定义的命令行参数似乎只有user,于是我们可以定义自己的。

本文中,我们将从一个INI文件读入COM的相关信息,同时,也提供注册选项注册到INI文件。注册命令定义为:

因此,首先改造ComModule::DllInstall如下:

 1STDMETHODIMP DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine)
 2{
 3    if (lpszCmdLine == nullptr)
 4    {
 5        return E_INVALIDARG;
 6    }
 7
 8    String strCmdLine = lpszCmdLine;
 9    String strCmdLineLower = strCmdLine.ToLower();
10
11    if (strCmdLineLower == _T("user"))
12    {
13        if (bInstall)
14        {
15            if (!RegisterTypeLib(HKEY_CURRENT_USER))
16            {
17                return E_FAIL;
18            }
19
20            if (!RegisterComClasses(HKEY_CURRENT_USER))
21            {
22                return E_FAIL;
23            }
24
25            return S_OK;
26        }
27        else
28        {
29            if (!UnregisterComClasses(HKEY_CURRENT_USER))
30            {
31                return E_FAIL;
32            }
33
34            if (!UnregisterTypeLib(HKEY_CURRENT_USER))
35            {
36                return E_FAIL;
37            }
38
39            return S_OK;
40        }
41    }
42
43    if (strCmdLineLower == _T("ini") || strCmdLineLower.IndexOf(_T("ini:")) == 0)
44    {
45        LPCTSTR DEFAULT_INI_FILENAME = _T("xlComReg.ini");
46        String strIniFileName = DEFAULT_INI_FILENAME;
47
48        if (strCmdLine.Length() > 4)
49        {
50            strIniFileName = strCmdLine.SubString(4);
51
52            if (strIniFileName[strIniFileName.Length() - 1] == _T('\\'))
53            {
54                strIniFileName += DEFAULT_INI_FILENAME;
55            }
56        }
57
58        if (bInstall)
59        {
60            if (!RegisterTypeLibToIni(strIniFileName))
61            {
62                return E_FAIL;
63            }
64
65            if (!RegisterComClassesToIni(strIniFileName))
66            {
67                return E_FAIL;
68            }
69
70            return S_OK;
71        }
72        else
73        {
74            if (!UnregisterComClassesFromIni(strIniFileName))
75            {
76                return E_FAIL;
77            }
78
79            if (!UnregisterTypeLibFromIni(strIniFileName))
80            {
81                return E_FAIL;
82            }
83
84            return S_OK;
85        }
86    }
87
88    return E_FAIL;
89}

默认INI名字定为xlComReg.ini。这里调用了四个函数:

RegisterTypeLibToIni

 1bool RegisterTypeLibToIni(const String &strIniFileName)
 2{
 3    if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("TypeLib"), m_strLibName))
 4    {
 5        return false;
 6    }
 7
 8    if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Version"), m_strLibVersion))
 9    {
10        return false;
11    }
12
13    String strModulePath = GetModuleRelativePathToIni(strIniFileName);
14
15#ifdef _WIN64
16    if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Win64"), strModulePath))
17    {
18        return false;
19    }
20#else
21    if (!IniFile::SetValue(strIniFileName, m_strLibID, _T("Win32"), strModulePath))
22    {
23        return false;
24    }
25#endif
26    return true;
27}

UnregisterComClassesFromIni

1bool UnregisterTypeLibFromIni(const String &strIniFileName)
2{
3    if (!IniFile::DeleteSection(strIniFileName, m_strLibID))
4    {
5        return false;
6    }
7
8    return true;      
9}

RegisterComClassesToIni

 1bool RegisterComClassesToIni(const String &strIniFileName)
 2{
 3    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
 4    {
 5        if (*ppEntry == nullptr)
 6        {
 7            continue;
 8        }
 9
10        TCHAR szClassID[40] = {};
11        StringFromGUID2(*(*ppEntry)->pClsid, szClassID, ARRAYSIZE(szClassID));
12
13        String strVersionIndependentProgID = (*ppEntry)->lpszProgID;
14        String strProgID = strVersionIndependentProgID + _T(".") + (*ppEntry)->lpszVersion;
15
16        if (!IniFile::SetValue(strIniFileName, szClassID, _T("Class"), (*ppEntry)->lpszClassDesc))
17        {
18            return false;
19        }
20
21        String strModulePath = GetModuleRelativePathToIni(strIniFileName);
22
23#ifdef _WIN64
24        if (!IniFile::SetValue(strIniFileName, szClassID, _T("InprocServer64"), strModulePath))
25        {
26            return false;
27        }
28#else
29        if (!IniFile::SetValue(strIniFileName, szClassID, _T("InprocServer32"), strModulePath))
30        {
31            return false;
32        }
33#endif
34
35        if (!m_strLibID.Empty())
36        {
37            if (!IniFile::SetValue(strIniFileName, szClassID, _T("TypeLib"), m_strLibID))
38            {
39                return false;
40            }
41        }
42
43        if (!strProgID.Empty())
44        {
45            if (!IniFile::SetValue(strIniFileName, szClassID, _T("ProgID"), strProgID))
46            {
47                return false;
48            }
49
50            if (!IniFile::SetValue(strIniFileName, strProgID, _T("Class"), (*ppEntry)->lpszClassDesc))
51            {
52                return false;
53            }
54
55            if (!IniFile::SetValue(strIniFileName, strProgID, _T("CLSID"), szClassID))
56            {
57                return false;
58            }
59        }
60
61        if (!strVersionIndependentProgID.Empty())
62        {
63            if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("Class"), (*ppEntry)->lpszClassDesc))
64            {
65                return false;
66            }
67
68            if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("CurVer"), strProgID))
69            {
70                return false;
71            }
72
73            if (!IniFile::SetValue(strIniFileName, strVersionIndependentProgID, _T("CLSID"), szClassID))
74            {
75                return false;
76            }
77        }
78    }
79
80    return true;
81}

UnregisterTypeLibFromIni

 1bool UnregisterComClassesFromIni(const String &strIniFileName)
 2{
 3    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
 4    {
 5        if (*ppEntry == nullptr)
 6        {
 7            continue;
 8        }
 9
10        TCHAR szClassID[40] = {};
11        StringFromGUID2(*(*ppEntry)->pClsid, szClassID, ARRAYSIZE(szClassID));
12
13        String strVersionIndependentProgID = (*ppEntry)->lpszProgID;
14        String strProgID = strVersionIndependentProgID + _T(".") + (*ppEntry)->lpszVersion;
15
16        if (!IniFile::DeleteSection(strIniFileName, szClassID))
17        {
18            return false;
19        }
20
21        if (!strProgID.Empty())
22        {
23            if (!IniFile::DeleteSection(strIniFileName, strProgID))
24            {
25                return false;
26            }
27        }
28
29        if (!strVersionIndependentProgID.Empty())
30        {
31            if (!IniFile::DeleteSection(strIniFileName, strVersionIndependentProgID))
32            {
33                return false;
34            }
35        }
36    }
37
38    return true;
39}

其中DLL路径用的是DLL相对于INI的相对路径,用函数GetModuleRelativePathToIni获取,该函数的实现如下:

 1String GetModuleRelativePathToIni(const String &strIniFileName)
 2{
 3    TCHAR szIniPathAbsolute[MAX_PATH] = {};
 4          
 5    if (GetFullPathName(strIniFileName.GetAddress(), ARRAYSIZE(szIniPathAbsolute), szIniPathAbsolute, nullptr) == 0)
 6    {
 7        return m_strModulePath;
 8    }
 9
10    TCHAR szModuleRelativePath[MAX_PATH] = {};
11
12    if (!PathRelativePathTo(szModuleRelativePath, szIniPathAbsolute, 0, m_strModulePath.GetAddress(), 0))
13    {
14        return m_strModulePath;
15    }
16
17    return szModuleRelativePath;
18}

上面代码将一个COM在注册表中的所有信息全部写到了INI。其实这是不必要的,对于C++程序来说,要使用这个COM,可只需要知道CLSID对应到哪个DLL就可以了。因此,上面划线的代码可以去掉不用,不影响后续使用。

好了,运行“regsvr32 /n /i:ini COMProvider.dll”,生成xlComReg.ini,内容如下:

1[{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}]
2Class=Streamlet COMProvider Sample Class
3InprocServer32=.\COMProvider.dll

加载

注册好了,该使用了。这就涉及COM的加载过程了。简单的说,我们一般先CoInitialize,然后CoCreateInstance拿到对象去使用,完了之后CoUninitialize使用完毕。现在我们就来模拟这个过程。除了这三个函数以外,我们还将模拟CoGetClassObject以及CoFreeUnusedLibraries。

于是加载器接口定义为:

1struct __declspec(uuid("FE52639A-5B41-49B0-9A50-7A1C4FBC83E2"))
2IComLoader : public IDispatch
3{
4    virtual HRESULT CoInitialize(_In_opt_ LPVOID pvReserved) PURE;
5    virtual void CoUninitialize() PURE;
6    virtual void CoFreeUnusedLibraries() PURE;
7    virtual HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) PURE;
8    virtual HRESULT CoCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv) PURE;
9};

ComLoader本身将作为一个Com类实现,因此我在IComLoader的声明中加上了UUID。然后我们针对注册到INI的COM写一个Loader。

相关数据结构定义如下:

 1typedef HRESULT (__stdcall *FnDllCanUnloadNow)();
 2typedef HRESULT (__stdcall *FnDllGetClassObject)(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv);
 3
 4struct ComDllModule
 5{
 6    String              strFileName;
 7    HMODULE             hModule;
 8    FnDllCanUnloadNow   fnDllCanUnloadNow;
 9    FnDllGetClassObject fnDllGetClassObject;
10
11    ComDllModule() : hModule(nullptr), fnDllCanUnloadNow(nullptr), fnDllGetClassObject(nullptr)
12    {
13      
14    }
15};
16
17typedef Map<String, String>       ClassIDPathMap;
18typedef Map<String, ComDllModule> PathModuleMap;

前面两行定义函数指针,这两个函数是COM DLL导出的。对于每个被加载的DLL,我们将查找这两个函数入口。ComDllModule结构用于保存一个已加载的COM DLL的信息。各分量意义很明白了,不解释。最后两个Map,一个是用于存储从INI读入的CLSID到DLL路径的对应关系,另一个是存储DLL加载后,DLL路径到ComDllModule结构的对应关系。

下面是ComLoaderFromIni的框架性定义:

 1class ComLoaderFromIni : public ComClass<ComLoaderFromIni>,
 2                            public Dispatcher<IComLoader>
 3{
 4public:
 5    ComLoaderFromIni(const String &strIniFile =_T("xlComReg.ini")) :
 6        m_strIniFile(strIniFile), m_lInitializeCount(0)
 7    {
 8      
 9    }
10
11    ~ComLoaderFromIni()
12    {
13        CoUninitialize();
14    }
15
16public:
17    HRESULT CoInitialize(_In_opt_ LPVOID pvReserved)
18    {
19        return S_OK;
20    }
21      
22    void CoUninitialize()
23    {
24
25    }
26      
27    void CoFreeUnusedLibraries()
28    {
29
30    }
31      
32    HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv)
33    {
34        return S_OK;
35    }
36
37private:
38    HRESULT FindComDllModule(REFCLSID rclsid, const ComDllModule **ppModule)
39    {
40        return S_OK;
41    }
42
43    bool LoadComDll(const String &strFileName)
44    {
45        return true;
46    }
47
48private:
49    String          m_strIniFile;
50    LONG            m_lInitializeCount;
51    ClassIDPathMap  m_mapClassIDToPath;
52    PathModuleMap   m_mapPathToModule;
53    CriticalSection m_cs;
54
55public:
56    XL_COM_INTERFACE_BEGIN(ComLoaderFromIni)
57        XL_COM_INTERFACE(IComLoader)
58        XL_COM_INTERFACE(IDispatch)
59    XL_COM_INTERFACE_END()
60};

其中主要函数目前还没实现。成员变量中有个m_lInitializeCount,是给CoInitialize和CoUninitialize做引用计数的,CriticalSection是给两个Map加锁用的。其余变量的意义很明白,也不介绍了。

LoadComDll

首先看最后一个函数,LoadComDll。它用于加载指定的COM DLL,并将模块信息存入Map。实现如下:

 1bool LoadComDll(const String &strFileName)
 2{
 3    XL_SCOPED_CRITICAL_SECTION(m_cs);
 4
 5    HMODULE hModule = LoadLibrary(strFileName.GetAddress());
 6
 7    if (hModule == nullptr)
 8    {
 9        return false;
10    }
11
12    ScopeGuard sgFreeLibrary = MakeGuard(Bind(FreeLibrary, hModule));
13
14    FnDllCanUnloadNow fnDllCanUnloadNow = (FnDllCanUnloadNow)GetProcAddress(hModule, "DllCanUnloadNow");
15
16    if (fnDllCanUnloadNow == nullptr)
17    {
18        return false;
19    }
20
21    FnDllGetClassObject fnDllGetClassObject = (FnDllGetClassObject)GetProcAddress(hModule, "DllGetClassObject");
22
23    if (fnDllGetClassObject == nullptr)
24    {
25        return false;
26    }
27
28    ComDllModule &module = m_mapPathToModule[strFileName];
29    module.strFileName = strFileName;
30    module.hModule = hModule;
31    module.fnDllCanUnloadNow = fnDllCanUnloadNow;
32    module.fnDllGetClassObject = fnDllGetClassObject;
33
34    sgFreeLibrary.Dismiss();
35
36    return true;
37}

FindComDllModule

倒数第二个函数,FindComDllModule,定义为从CLSID找到ComDllModule。首先从m_mapClassIDToPath找到路径,再尝试从m_mapPathToModule找到ComModule。如果未找到,那就尝试使用上面的LoadComDll加载它。代码如下:

 1HRESULT FindComDllModule(REFCLSID rclsid, const ComDllModule **ppModule)
 2{
 3    XL_SCOPED_CRITICAL_SECTION(m_cs);
 4
 5    if (ppModule == nullptr)
 6    {
 7        return E_INVALIDARG;
 8    }
 9
10    *ppModule = nullptr;
11      
12    TCHAR szClassID[40] = {};
13    StringFromGUID2(rclsid, szClassID, ARRAYSIZE(szClassID));
14
15    auto itPath = m_mapClassIDToPath.Find(szClassID);
16
17    if (itPath == m_mapClassIDToPath.End())
18    {
19        return REGDB_E_CLASSNOTREG;
20    }
21
22    auto itModule = m_mapPathToModule.Find(itPath->Value);
23
24    if (itModule == m_mapPathToModule.End())
25    {
26        if (!LoadComDll(itPath->Value))
27        {
28            return E_FAIL;
29        }
30
31        itModule = m_mapPathToModule.Find(itPath->Value);
32    }
33
34    if (itModule == m_mapPathToModule.End())
35    {
36        return E_FAIL;
37    }
38
39    *ppModule = &itModule->Value;
40
41    return S_OK;
42}

CoInitialize

下面按使用流程分别介绍五个标准函数。首先是CoInitialize,它主要就是从INI读取CLSID到DLL路径的对应关系,代码如下:

 1HRESULT CoInitialize(_In_opt_ LPVOID pvReserved)
 2{
 3    XL_SCOPED_CRITICAL_SECTION(m_cs);
 4
 5    if (m_lInitializeCount > 0)
 6    {
 7        InterlockedIncrement(&m_lInitializeCount);
 8        return S_FALSE;
 9    }
10
11    Array<String> arrSections;
12
13    if (!IniFile::EnumSections(m_strIniFile, &arrSections))
14    {
15        return E_FAIL;
16    }
17
18    for (auto it = arrSections.Begin(); it != arrSections.End(); ++it)
19    {
20        String strClass;
21
22        if (!IniFile::GetValue(m_strIniFile, *it, _T("Class"), &strClass))
23        {
24            continue;
25        }
26
27        String strPath;
28
29#ifdef _WIN64
30        if (!IniFile::GetValue(m_strIniFile, *it, _T("InprocServer64"), &strPath))
31        {
32            continue;
33        }
34#else
35        if (!IniFile::GetValue(m_strIniFile, *it, _T("InprocServer32"), &strPath))
36        {
37            continue;
38        }
39#endif
40
41        m_mapClassIDToPath.Insert(*it, strPath);
42    }      
43
44    InterlockedIncrement(&m_lInitializeCount);
45
46    return S_OK;
47}

需要留意的就是引用计数处理,这使得多次调用CoInitialze也是安全的。

CoGetClassObject

这个函数用于取得类厂。由于上面已经准备了FindComDllModule,直接调用获取到ComDllModule信息,然后调用COM DLL导出的DllGetClassObject就可以了:

 1HRESULT CoGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv)
 2{
 3    XL_SCOPED_CRITICAL_SECTION(m_cs);
 4
 5    const ComDllModule *pModule = nullptr;
 6    HRESULT hr = FindComDllModule(rclsid, &pModule);
 7
 8    if (FAILED(hr))
 9    {
10        return hr;
11    }
12
13    return pModule->fnDllGetClassObject(rclsid, riid, ppv);
14}

CoCreateInstance

由于上面已经可以拿到类厂了,这里直接调用,获取类厂后调用类厂的CreateInstance创建对象:

 1HRESULT CoCreateInstance(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID *ppv)
 2{
 3    IClassFactory *pClassFactory = nullptr;
 4    HRESULT hr = CoGetClassObject(rclsid, __uuidof(IClassFactory), (LPVOID *)&pClassFactory);
 5
 6    if (FAILED(hr))
 7    {
 8        return hr;
 9    }
10
11    hr = pClassFactory->CreateInstance(NULL, riid, ppv);
12    pClassFactory->Release();
13
14    return hr;
15}

CoFreeUnusedLibraries

这个函数用于清理不再使用的COM DLL。实现思路就是遍历已加载的模块信息,调用DllCanUnloadNow,如果DLL返回S_OK,将其卸载:

 1void CoFreeUnusedLibraries()
 2{
 3    XL_SCOPED_CRITICAL_SECTION(m_cs);
 4
 5    for (auto it = m_mapPathToModule.Begin(); it != m_mapPathToModule.End(); )
 6    {
 7        if (it->Value.fnDllCanUnloadNow() == S_OK)
 8        {
 9            FreeLibrary(it->Value.hModule);
10            it = m_mapPathToModule.Delete(it);
11        }
12        else
13        {
14            ++it;
15        }
16    }
17}

CoUninitialize

这是最终的卸载函数,卸载所有已加载的DLL,清除所有信息:

 1void CoUninitialize()
 2{
 3    XL_SCOPED_CRITICAL_SECTION(m_cs);
 4
 5    if (m_lInitializeCount == 0)
 6    {
 7        return;
 8    }
 9
10    InterlockedDecrement(&m_lInitializeCount);;
11
12    if (m_lInitializeCount > 0)
13    {
14        return;
15    }
16
17    for (auto it = m_mapPathToModule.Begin(); it != m_mapPathToModule.End(); ++it)
18    {
19        FreeLibrary(it->Value.hModule);
20    }
21
22    m_mapClassIDToPath.Clear();
23    m_mapPathToModule.Clear();
24}

需要注意的是,这里也有引用计数的处理,与CoInitialize重的对应。

使用

使用之前,先为刚才的加载器创建一个工厂函数吧(当然,直接使用也没关系):

 1enum ComLoadType
 2{
 3    CLT_FROM_INI,
 4};
 5
 6inline IComLoader *CreateComLoader(ComLoadType type, const String &strData = _T("xlComReg.ini"))
 7{
 8    IComLoader *pLoader = nullptr;
 9
10    switch (type)
11    {
12    case CLT_FROM_INI:
13        pLoader = new ComLoaderFromIni(strData);
14        pLoader->AddRef();
15        break;
16    default:
17        break;
18    }
19
20    return pLoader;
21}

目前只有INI加载器。

使用方式如下:

 1int _tmain(int argc, TCHAR *argv[])
 2{
 3    xl::IComLoader *pComLoader = xl::CreateComLoader(xl::CLT_FROM_INI);
 4    HRESULT hr = pComLoader->CoInitialize(NULL);
 5
 6    ISampleInterface *pSampleInterface = nullptr;
 7    hr = pComLoader->CoCreateInstance(__uuidof(SampleClass),
 8                                      __uuidof(ISampleInterface),
 9                                      (LPVOID *)&pSampleInterface);
10
11    if (SUCCEEDED(hr))
12    {
13        pSampleInterface->SampleMethod();
14        pSampleInterface->Release();
15    }
16
17    pComLoader->Release();
18
19    return 0;
20}

运行结果:

以上,框架代码见: http://xllib.codeplex.com/SourceControl/changeset/view/20034#319450 例子代码见COMProtocol4.rarhttp://pan.baidu.com/s/1qWJSeS0

在本文中,我们脱离了注册表依赖,将COM信息存到了本目录文件,这意味着我们在发布的时候可以事先生成这个文件,或者可以由安装程序生成。至此,我们给出了使用COM DLL作为替代普通DLL的一整套方案。如果能忍受COM接口讨厌的有限的变量类型,以及冗长的行文方式,COM DLL的形式将比普通DLL导出函数更具有优势,可以作为动态链接库实现形式的一般解决方案。

等等,有人可能会问:套间模型哪去了呢?

——套间模型是什么?这显然不影响我们将COM DLL用于替代普通DLL的整个过程。不需要这个概念。


首发:http://www.cppblog.com/Streamlet/archive/2012/09/21/191436.html



NoteIsSite/0.4